package edu.washington.cs.workflow.analysis;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.ibm.wala.classLoader.IClass;
import com.ibm.wala.ipa.callgraph.CGNode;
import com.ibm.wala.ipa.callgraph.CallGraph;
import com.ibm.wala.ipa.callgraph.propagation.AllocationSiteInNode;
import com.ibm.wala.ipa.callgraph.propagation.ConcreteTypeKey;
import com.ibm.wala.ipa.callgraph.propagation.InstanceKey;
import com.ibm.wala.ipa.callgraph.propagation.LocalPointerKey;
import com.ibm.wala.ipa.callgraph.propagation.PointerAnalysis;
import com.ibm.wala.ipa.callgraph.propagation.PointerKey;
import com.ibm.wala.ipa.cha.ClassHierarchy;
import com.ibm.wala.ssa.SSAInstruction;
import com.ibm.wala.ssa.SSAInvokeInstruction;
import com.ibm.wala.types.TypeName;

import edu.washington.cs.workflow.util.Utils;
import edu.washington.cs.workflow.util.WALAUtils;

/**
 * Given an event handler, returns a set of possible UI actions that may trigger it
 * */
public class UIActionIdentifier {

	public final AnalysisBuilder builder;
	private CallGraph cg = null;
	public final PointerAnalysis pa;
	public final String[] appPackages;
	public final ClassHierarchy cha;
	
	private EventHandlerManager eventManager = new EventHandlerManager();
	private GUILabelIdentifier labelIdentifier = new GUILabelIdentifier();
	
	public UIActionIdentifier(AnalysisBuilder builder) {
		Utils.checkNotNull(builder);
		this.builder = builder;
		this.cg = builder.getCallGraph();
		this.pa = builder.getPointerAnalysis();
		this.appPackages = builder.appPackages;
		this.cha = builder.getClassHierarchy();
	}
	
	/**
	 * This must be a handler
	 * */
	public List<GUIObjectInstance> findUIActionObjects(CGNode handler) {
		//First, get places that initialize the handler class
		IClass clz = handler.getMethod().getDeclaringClass();
		
		//check the addListener methods
		Collection<PointerKey> localVars = this.getAllocationSites(this.pa, clz);
		
		System.out.println("Number of local vars: " + localVars.size());
		
		//get the receiver object and check its object types
		List<GUIObjectInstance> retList = new LinkedList<GUIObjectInstance>();
		for (PointerKey localVar : localVars) {
			Utils.checkNotNull(localVar);
			Utils.checkTrue(localVar instanceof LocalPointerKey,
					"Not a LocalPointerKey type. It is: " + localVar.getClass());
			LocalPointerKey lpKey = (LocalPointerKey) localVar;
			//find the event-triggering object info
			Collection<GUIObjectInstance> objects
			    = this.findEventTriggeringGUIObjects(lpKey.getNode(), lpKey.getValueNumber());
			Utils.checkNotNull(objects);
			retList.addAll(objects);
		}

		return retList;
	}
	
	/**
	 * Be aware: calling this method can be very expensive
	 * */
	private Map<GUIObjectInstance, Set<String>> cachedMap = null;
	private void buildTheActionMethodMap() {
		if(cachedMap != null) {
			return;
		}
		System.out.println("Building cached map....");
		cachedMap = new HashMap<GUIObjectInstance, Set<String>>();
		EventReachableMethodFinder finder = new EventReachableMethodFinder(builder);
		Collection<CGNode> handlers = finder.getAllEventHandlers();
		System.out.println("Total num of handlers: " + handlers.size());
		for(CGNode handler : handlers) {
			List<GUIObjectInstance> objs = this.findUIActionObjects(handler);
			System.out.println("  -- " + objs.size() + " obj instances for handler: " + handler);
			if(!objs.isEmpty()) {
				Collection<CGNode> reachables = finder.getReachableNode(handler);
				System.out.println("    + number of reachable nodes: " + reachables.size());
				//each GUI Object instance ==> reachables
				for(GUIObjectInstance obj : objs) {
					if(!cachedMap.containsKey(obj)) {
						cachedMap.put(obj, new HashSet<String>());
					}
					for(CGNode n : reachables) {
						cachedMap.get(obj).add(n.getMethod().getSignature());
					}
				}
				//reclaim memory
				reachables.clear();
			}
		}
		System.out.println("Finish building cached map.");
	}
	
	private Map<String, Set<String>> cachedResults = new HashMap<String, Set<String>>();
	public Set<String> getGUIObjectInstances(String methodDesc) {
		if(cachedResults.containsKey(methodDesc)) {
			return cachedResults.get(methodDesc);
		}
		this.buildTheActionMethodMap();
		Utils.checkNotNull(cachedMap);
		
		Set<String> instances = new HashSet<String>();
		
		for(GUIObjectInstance action : cachedMap.keySet()) {
			if(cachedMap.get(action).contains(methodDesc)) {
				instances.add(action.toString());
			}
		}
		
		//add to the cache
		cachedResults.put(methodDesc, instances);
		
		return instances;
	}
	
	private Collection<PointerKey> getAllocationSites(PointerAnalysis pa, IClass clazz) {
		Collection<PointerKey> keys = new LinkedList<PointerKey>();
		for(PointerKey key : pa.getPointerKeys()) {
			if(pointsToAllocations(pa, key, clazz, this.appPackages)) {
				keys.add(key);
			}
		}
		return keys;
	}
	
	/**
	 * For each pointer key, should it be included. Need to check whether it is an allocation
	 * site, and whether it is in the user package, allocate the given class instance, and it
	 * is a local key.
	 * */
	private boolean pointsToAllocations(PointerAnalysis pa, PointerKey key,
			IClass clazz, String[] appPackages) {
		if(!(key instanceof LocalPointerKey)) {
			return false;
		}
		for(InstanceKey instanceKey : pa.getPointsToSet(key)) {
			if(instanceKey instanceof AllocationSiteInNode) {
				AllocationSiteInNode allocSite = (AllocationSiteInNode)instanceKey;
				TypeName tName = allocSite.getSite().getDeclaredType().getName();
				//check the type name, allocate the given class?
				if(!tName.equals(clazz.getName())) {
					continue;
				}
			} else if (instanceKey instanceof ConcreteTypeKey) {
				ConcreteTypeKey concreteType = (ConcreteTypeKey)instanceKey;
				TypeName tName = concreteType.getConcreteType().getName();
//					allocSite.getSite().getDeclaredType().getName();
				//check the type name, allocate the given class?
				if(!tName.equals(clazz.getName())) {
					continue;
				}
			} else {
				continue;
			}
			LocalPointerKey lpKey = (LocalPointerKey)key;
			CGNode hostNode = lpKey.getNode();
		    String pkgName = WALAUtils.getJavaPackageName(hostNode.getMethod().getDeclaringClass()); 
		    if(Utils.startWith(pkgName, appPackages)) {
		    	return true;
		    }
		}
		return false;
	}
	
	
	/**
	 * Given a call graph node, and a variable that represents an instance of an event-handling
	 * object. Find all objects that add this "event-handling object" as a listener object.
	 * This works in an intra-procedural way.
	 * */
	protected Collection<GUIObjectInstance> findEventTriggeringGUIObjects(CGNode node, int valueNum) {
		Collection<GUIObjectInstance> retObjs = new LinkedHashSet<GUIObjectInstance>();
		List<SSAInstruction> irList = WALAUtils.getAllIRs(node);
		for(SSAInstruction ssa : irList) {
			//interested in the invocation instruction
			if(ssa instanceof SSAInvokeInstruction) {
				SSAInvokeInstruction invoke = (SSAInvokeInstruction)ssa;
				if(!this.eventManager.isEventHandlerAdditionCall(invoke)) { //e.g., addActionListener
					continue;
				}
				if(useVarInInstruction(valueNum, invoke)) {
					//e.g., return use(0) for instruction like: jbutton.addActionListener
					int guiObjVarIndex = this.labelIdentifier.getGUIIndexInActionAdding(invoke);
					//look for the method call like: <init> or setLabel which may contain label data
					
					System.out.println("found: " + invoke);
					
					//a simple case
					GUIObjectInstance instance = new GUIObjectInstance(node, invoke.toString());
					retObjs.add(instance);
//					}
				}
			}
		}
		
		return retObjs;
	}
	
	/**
	 * returns button for button.<init>
	 * */
	static String getReceiverObjTypeName(SSAInvokeInstruction instr) {
		Utils.checkTrue(!instr.isStatic());
		Utils.checkTrue(instr.getNumberOfUses() > 0);
		return instr.getDeclaredTarget().getDeclaringClass().getName().toString();
	}
	
	static boolean useVarInInstruction(int valueNum, SSAInstruction ssa) {
		for(int i = 0; i < ssa.getNumberOfUses(); i++) {
			if(ssa.getUse(i) == valueNum) {
				return true;
			}
		}
		return false;
	}
}
